﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using Evaluant.OPath.Expressions;
using Evaluant.OPath.Expressions.Collections;

using OPathExpression = Evaluant.OPath.Expressions.Expression;
using Expression = System.Linq.Expressions.Expression;
using Evaluant.Uss.ObjectContext;

namespace Evaluant.Uss.Linq
{
    internal class QueryTranslator : ExpressionVisitor
    {
        Path path = new Path();

        private Constraint result;

        Dictionary<string, Identifier> identifiers = new Dictionary<string, Identifier>();
        Identifier currentIdentifier;
        ObjectContext.ObjectContext oc;

        internal QueryTranslator(ObjectContext.ObjectContext oc)
        {
            this.oc = oc;
        }

        public bool SelectMany { get; set; }

        public Path Path
        {
            get { return path; }
        }

        public Dictionary<string, Identifier> Identifiers
        {
            get { return identifiers; }
        }

        public Identifier CurrentIdentifier
        {
            get { return currentIdentifier; }
        }

        internal QueryTranslator Translate(Expression expression)
        {
            this.Visit(expression);
            return this;
        }

        public Constraint Result
        {
            get { return result; }
        }

        private static Expression StripQuotes(Expression e)
        {
            while (e.NodeType == ExpressionType.Quote)
            {
                e = ((UnaryExpression)e).Operand;
            }
            return e;
        }

        protected override Expression VisitMethodCall(MethodCallExpression m)
        {
            if (m.Method.DeclaringType == typeof(string))
            {
                this.Visit(m.Object);
                Constraint left = result;

                this.Visit(m.Arguments[0]);
                Constraint right = result;

                switch (m.Method.Name)
                {
                    case "Contains":
                        result = new BinaryOperator(BinaryOperatorEnum.Contains, left, right);
                        return m;

                    case "StartsWith":
                        result = new BinaryOperator(BinaryOperatorEnum.BeginsWith, left, right);
                        return m;

                    case "EndsWith":
                        result = new BinaryOperator(BinaryOperatorEnum.EndsWith, left, right);
                        return m;
                }

            }

            if (typeof(System.Collections.IEnumerable).IsAssignableFrom(m.Method.DeclaringType) || m.Method.DeclaringType == typeof(Enumerable))
            {
                string name; // cp.Pieces
                string variable; // cp
                string ident; // Pieces

                if (m.Arguments[0].NodeType == ExpressionType.Constant)
                {
                    name = m.Object.ToString();
                    variable = ((ParameterExpression)((MemberExpression)m.Object).Expression).Name;
                    ident = ((MemberExpression)m.Object).Member.Name;
                }
                else if (!SelectMany)
                {
                    name = ((ParameterExpression)((MemberExpression)m.Arguments[0]).Expression).Name + "." + ((MemberExpression)m.Arguments[0]).Member.Name;
                    // cp
                    variable = ((ParameterExpression)((MemberExpression)m.Arguments[0]).Expression).Name; ; //(m.Arguments[0]).Expression.ToString();
                    ident = ((MemberExpression)m.Arguments[0]).Member.Name;
                }
                else
                {
                    name = ((MemberExpression)m.Arguments[0]).Member.Name;
                    // cp
                    variable = ((ParameterExpression)((MemberExpression)m.Arguments[0]).Expression).Name; ; //(m.Arguments[0]).Expression.ToString();
                    ident = ((MemberExpression)m.Arguments[0]).Member.Name;
                }


                if (!identifiers.ContainsKey(name))
                {
                    identifiers.Add(name, new Identifier(ident, null));

                    if (!identifiers.ContainsKey(variable))
                    {
                        Identifier i = identifiers[String.Empty];
                        identifiers.Add(variable, i);
                        identifiers.Remove(String.Empty);
                    }

                }
                Path localPath = new Path();
                localPath.Identifiers.Add(Identifiers[name]);
                // Evaluates the expression if a lambda expression is passed in the Count( l => l...)
                if (m.Arguments.Count > 1)
                {
                    if (!Identifiers.ContainsKey(((LambdaExpression)m.Arguments[1]).Parameters[0].Name))
                        Identifiers.Add(((LambdaExpression)m.Arguments[1]).Parameters[0].Name, identifiers[name]);
                    this.Visit(m.Arguments[1]);
                    if (result is Path)
                    {
                        localPath.Identifiers.AddRange(((Path)result).Identifiers);
                    }
                    else
                        Identifiers[name].Constraint = result;
                }
                else
                    Identifiers[name].Constraint = result;

                FunctionEnum function = FunctionEnum.Unknown;

                switch (m.Method.Name)
                {
                    case "Count":
                        function = FunctionEnum.Count;
                        break;
                    case "Min":
                        function = FunctionEnum.Min;
                        break;
                    case "Max":
                        function = FunctionEnum.Max;
                        break;
                    case "Average":
                        function = FunctionEnum.Average;
                        break;
                    case "Contains":
                        function = FunctionEnum.Exists;

                        string id = oc.GetId(((ConstantExpression)m.Arguments[0]).Value);

                        identifiers[name].Constraint = new Call("id", new ConstraintCollection(new Constraint[] { new Value(id, ValueEnum.String) }));
                        break;
                }

                result = new Function(localPath, function);
                currentIdentifier = identifiers[variable];

                // Removes the local variable if a lambda expression is passed in the Count( l => l...)
                if (m.Arguments.Count > 1)
                {
                    Identifiers.Remove(((LambdaExpression)m.Arguments[1]).Parameters[0].Name);
                }

                return m;
            }

            // Handles LINQ methods

            throw new NotSupportedException(string.Format("The method '{0}' is not supported", m.Method.Name));
        }

        protected override Expression VisitUnary(UnaryExpression u)
        {
            switch (u.NodeType)
            {
                case ExpressionType.Not:

                    if (u.Operand is MemberExpression && u.Operand.Type == typeof(Boolean))
                    {
                        this.VisitBinary(Expression.Equal(u.Operand, Expression.Constant(false)));
                    }
                    else
                    {
                        this.Visit(u.Operand);
                        result = new UnaryOperator(UnaryOperatorEnum.Not, result);
                    }


                    break;
                case ExpressionType.Convert:
                    if (u.Operand.Type.IsEnum)
                    {
                        this.Visit(u.Operand);
                        break;
                    }

                    // Implicit conversion when comparing two values, return the operand
                    this.Visit(u.Operand);
                    break;

                default:
                    throw new NotSupportedException(string.Format("The unary operator '{0}' is not supported", u.NodeType));
            }
            return u;
        }

        protected override Expression VisitBinary(BinaryExpression b)
        {
            this.Visit(b.Left);
            Constraint left = result;

            this.Visit(b.Right);
            Constraint right = result;

            BinaryOperatorEnum op = BinaryOperatorEnum.Unknown;

            switch (b.NodeType)
            {
                case ExpressionType.And:
                    op = BinaryOperatorEnum.And;
                    break;
                case ExpressionType.AndAlso:
                    op = BinaryOperatorEnum.And;
                    break;
                case ExpressionType.Or:
                    op = BinaryOperatorEnum.Or;
                    break;
                case ExpressionType.OrElse:
                    op = BinaryOperatorEnum.Or;
                    break;
                case ExpressionType.Equal:

                    // Enum predicates
                    if (b.Left.NodeType == ExpressionType.Convert && ((UnaryExpression)b.Left).Operand.Type.IsEnum)
                    {
                        right = new Value(Enum.Parse(((UnaryExpression)b.Left).Operand.Type, b.Right.ToString()).ToString(), ValueEnum.String);
                    }
                    else
                        if (b.Right.NodeType == ExpressionType.Convert && ((UnaryExpression)b.Right).Operand.Type.IsEnum)
                        {
                            left = new Value(Enum.Parse(((UnaryExpression)b.Right).Operand.Type, b.Left.ToString()).ToString(), ValueEnum.String);
                        }

                        else
                            if (left is Call && ((Call)left).Name == "id")
                            {
                                ((Call)left).Operands.Add(right);
                                result = left;

                                return b;
                            }

                    // cp.Piece == null predicate (to-one relationship)
                    if (right is Value && ((Value)right).Text == "null")
                    {
                        result = new UnaryOperator(UnaryOperatorEnum.Not, new Function(new Path(new IdentifierCollection(new Identifier[] { new Identifier(left.ToString(), null) })), FunctionEnum.Exists));
                        return b;
                    }

                    op = BinaryOperatorEnum.Equal;
                    break;
                case ExpressionType.NotEqual:

                    // Enum predicates
                    if (b.Left.NodeType == ExpressionType.Convert && ((UnaryExpression)b.Left).Operand.Type.IsEnum)
                    {
                        right = new Value(Enum.Parse(((UnaryExpression)b.Left).Operand.Type, b.Right.ToString()).ToString(), ValueEnum.String);
                    }
                    else
                        if (b.Right.NodeType == ExpressionType.Convert && ((UnaryExpression)b.Right).Operand.Type.IsEnum)
                        {
                            left = new Value(Enum.Parse(((UnaryExpression)b.Right).Operand.Type, b.Left.ToString()).ToString(), ValueEnum.String);
                        }

                        else
                            //cp.Id != value predicate
                            if (left is Call && ((Call)left).Name == "id")
                            {
                                ((Call)left).Operands.Add(right);
                                result = new UnaryOperator(UnaryOperatorEnum.Not, left);

                                return b;
                            }

                    // cp.Piece != null predicate (to-one relationship)
                    if (right is Value && ((Value)right).Text == "null")
                    {
                        result = new Function(new Path(new IdentifierCollection(new Identifier[] { new Identifier(left.ToString(), null) })), FunctionEnum.Exists);
                        return b;
                    }

                    op = BinaryOperatorEnum.NotEqual;
                    break;
                case ExpressionType.LessThan:
                    op = BinaryOperatorEnum.Lesser;
                    break;
                case ExpressionType.LessThanOrEqual:
                    op = BinaryOperatorEnum.LesserOrEqual;
                    break;
                case ExpressionType.GreaterThan:
                    op = BinaryOperatorEnum.Greater;
                    break;
                case ExpressionType.GreaterThanOrEqual:
                    op = BinaryOperatorEnum.GreaterOrEqual;
                    break;
                default:
                    throw new NotSupportedException(string.Format("The binary operator '{0}' is not supported", b.NodeType));
            }

            result = new BinaryOperator(op, left, right);
            return b;
        }

        private string FormatString(string s)
        {
            bool containsQuote = s.IndexOf("'") != -1;
            bool containsBSlash = s.IndexOf(@"\") != -1;

            if (containsQuote || containsBSlash)
            {
                return s.Replace(@"\", @"\\").Replace("'", @"\'");
            }

            return s;
        }

        protected override Expression VisitConstant(ConstantExpression c)
        {
            IQueryable q = c.Value as IQueryable;

            if (c.Value == null)
            {
                result = new Value("null", ValueEnum.Unknown);
            }
            else
            {
                switch (Type.GetTypeCode(c.Value.GetType()))
                {
                    case TypeCode.Boolean:
                        result = (bool)c.Value ? new Value("true", ValueEnum.Boolean) : new Value("false", ValueEnum.Boolean);
                        break;

                    case TypeCode.String:

                        result = new Value(FormatString((string)c.Value), ValueEnum.String);
                        break;

                    case TypeCode.Int16:
                    case TypeCode.Int32:
                    case TypeCode.Int64:
                    case TypeCode.UInt16:
                    case TypeCode.UInt32:
                    case TypeCode.UInt64:
                    case TypeCode.Byte:
                    case TypeCode.SByte:
                        result = new Value(c.Value.ToString(), ValueEnum.Integer);
                        break;

                    case TypeCode.Decimal:
                    case TypeCode.Double:
                    case TypeCode.Single:
                        result = new Value(Convert.ToString(c.Value, System.Globalization.CultureInfo.CreateSpecificCulture("en-US")), ValueEnum.Float);
                        break;

                    case TypeCode.DateTime:
                        result = new Value(c.Value.ToString(), ValueEnum.Date);
                        break;

                    default:
                        throw new NotSupportedException(string.Format("The constant for '{0}' is not supported", c.Value));
                }
            }
            return c;
        }

        protected override Expression VisitMemberAccess(MemberExpression m)
        {
            string memberName = m.Member.Name;

            // Parameter: cp.Identifiant
            if (m.Expression != null && m.Expression.NodeType == ExpressionType.Parameter)
            {
                string name = m.Expression.ToString();

                if (!identifiers.ContainsKey(name))
                {
                    // if no name is specified and String.Empty is not in the dictionary, use the only available identifier (in case of anonymous constraints e.g. OrderBy( => .Name))
                    if (name != String.Empty || identifiers.ContainsKey(String.Empty))
                    {
                        Identifier i = identifiers[String.Empty];
                        identifiers.Add(name, i);
                        identifiers.Remove(String.Empty);
                    }
                }

                if (name == string.Empty)
                {
                    currentIdentifier = identifiers[identifiers.Keys.ElementAt<string>(identifiers.Count - 1)];
                }
                else
                {
                    currentIdentifier = identifiers[name];
                }

                Evaluant.Uss.ObjectContext.Descriptors.PropertyDescriptor prop = oc.Factory.PersistentDescriptor.GetIdDescriptor(m.Member.ReflectedType);

                if ((prop != null && prop.PropertyName == memberName) || (memberName == "Id" && typeof(IPersistable).IsAssignableFrom(m.Member.ReflectedType)))
                {
                    result = new Call("id", new ConstraintCollection());
                }
                else
                {
                    result = new Path(new IdentifierCollection(new Identifier[] { new Identifier(memberName, null) }));

                    //if(m.Type == typeof(Boolean))
                    //{
                    //    result = new BinaryOperator(BinaryOperatorEnum.Equal, result, new Value("true", ValueEnum.Boolean));
                    //}
                }

                return m;
            }

            // Member: cp.Piece.Identifiant
            if (m.Expression != null && m.Expression.NodeType == ExpressionType.MemberAccess)
            {
                // cp.Piece
                string name;
                if (!SelectMany)
                {
                    name = ((ParameterExpression)((MemberExpression)m.Expression).Expression).Name + "." + ((MemberExpression)m.Expression).Member.Name;
                }
                else
                {
                    if (((MemberExpression)m.Expression).Expression is MemberExpression)
                        Visit(m.Expression);
                    name = ((MemberExpression)m.Expression).Member.Name;
                }

                if (currentIdentifier == null)
                {

                    if (!identifiers.ContainsKey(name))
                    {
                        identifiers.Add(name, new Identifier(((MemberExpression)m.Expression).Member.Name, null));

                        // cp
                        string variable = ((MemberExpression)m.Expression).Expression.ToString();
                        if (!identifiers.ContainsKey(variable))
                        {
                            Identifier i = identifiers[String.Empty];
                            identifiers.Add(variable, i);
                            identifiers.Remove(String.Empty);
                        }

                        identifiers[variable].Constraint = new Function(new Path(new IdentifierCollection(new Identifier[] { identifiers[name] })), FunctionEnum.Exists);
                    }

                    currentIdentifier = identifiers[name];
                }

                Evaluant.Uss.ObjectContext.Descriptors.PropertyDescriptor prop = oc.Factory.PersistentDescriptor.GetIdDescriptor(m.Member.ReflectedType);

                if ((prop != null && prop.PropertyName == memberName) || (memberName == "Id" && typeof(IPersistable).IsAssignableFrom(m.Member.ReflectedType)))
                {
                    if (result != null && result is Path)
                    {
                        Call call = new Call("id", new ConstraintCollection());
                        ((Path)result).Identifiers[((Path)result).Identifiers.Count - 1].Constraint = call;
                        result = call;
                    }
                    else
                        result = new Call("id", new ConstraintCollection());

                    if (currentIdentifier.Constraint == null)
                        currentIdentifier.Constraint = result;
                }
                else
                {
                    if (result != null && result is Path)
                    {

                        if (!prop.Type.IsPrimitive && prop.Type != typeof(string))
                            currentIdentifier.Constraint = new Function((Path)result, FunctionEnum.Exists);
                        else
                            currentIdentifier.Constraint = result;

                        ((Path)result).Identifiers.Add(new Identifier(memberName, null));

                        if (currentIdentifier.Constraint == null)
                            currentIdentifier.Constraint = result;
                    }
                    else
                    {
                        result = new Path(new IdentifierCollection(new Identifier[] { new Identifier(memberName, null) }));

                        if (prop != null && !prop.Type.IsPrimitive && prop.Type != typeof(string))
                            currentIdentifier.Constraint = new Function((Path)result, FunctionEnum.Exists);
                    }
                }

                if (m.Type == typeof(Boolean))
                {
                    result = new BinaryOperator(BinaryOperatorEnum.Equal, result, new Value("true", ValueEnum.Boolean));

                }

                return m;
            }

            throw new NotSupportedException(string.Format("The member '{0}' is not supported", memberName));
        }

        internal void Where(Expression e)
        {
            this.Translate(e);

            if (currentIdentifier.Constraint == null || result.ToString().StartsWith(currentIdentifier.Constraint.ToString()))
                currentIdentifier.Constraint = result;
            else if (currentIdentifier.Constraint.ToString() != result.ToString())
                currentIdentifier.Constraint = new BinaryOperator(BinaryOperatorEnum.And, currentIdentifier.Constraint, result);

            currentIdentifier = null;
        }
    }
}
